﻿namespace DCHealthCheck {

    #region Usings
    using DCHealthCheck.Common;
    using JHSoftware;
    using Microsoft.Win32;
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Diagnostics;
    using System.DirectoryServices.Protocols;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Management;
    using System.Net;
    using System.Net.Mail;
    using System.Net.Mime;
    using System.Net.NetworkInformation;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading; 
    #endregion

    public struct PingResult {
        public int RoundTripTimeMs;
        public double PercentLoss;
    }

    public class Utility {

        #region Members
        private static readonly int DNSLookupTimeoutSeconds;

        /// <summary>
        /// For Domain Controllers that return multiple addresses in response to System.Net.Dns.GetHostEntry,
        /// we will filter out addresses that start with these strings.
        /// </summary>
        private static List<string> IPAddressFilters { get; set; }

        private static log4net.ILog Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private static readonly int NetworkConnectionTimeoutSeconds;

        private static readonly string PingPayload;

        private static readonly int PingPayloadSize;

        private static readonly int PingTimeoutSeconds;

        private static string PSExecPath { get; set; }

        private static readonly DateTime SqlDateTimeMinValue = new DateTime(1753, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        private static readonly object SyncRoot = new object();

        #region Email notification
        private static string EmailBccRecipients { get; set; }

        private static string EmailRecipients { get; set; }

        private static string EmailSender { get; set; }

        private static string EmailSubject { get; set; }

        private static string SMTPServer { get; set; }

        private static Stack<MailMessage> MailMessageStack {
            get {
                if (mailMessageStack == null) {
                    lock (SyncRoot) {
                        if (mailMessageStack == null) {
                            mailMessageStack = new Stack<MailMessage>();
                        }
                    }
                }

                if (mailMessageStack.Count > 10000) {
                    lock (SyncRoot) {
                        if (mailMessageStack.Count > 10000) {
                            Log.Warn("Resetting MailMessageStack due to max capacity of 10000 reached");
                        }
                        mailMessageStack.Clear();
                    }
                }
                return mailMessageStack;
            }
            set { mailMessageStack = value; }
        }
        private static Stack<MailMessage> mailMessageStack;

        private static string TestEmailBccRecipients { get; set; }
        #endregion

        #region Port numbers
        private const int LdapPort = 389;
        private static readonly int LdapSSLPort = 636;
        private const int GcPort = 3268;
        private static readonly int GcSSLPort = 3269;
        #endregion 
        #endregion

        #region Constructor
        static Utility() {
            PingPayloadSize = ConfigurationManager.AppSettings["PingPayloadSize"] != null ? Convert.ToInt32(ConfigurationManager.AppSettings["PingPayloadSize"]) : 1172;
            DNSLookupTimeoutSeconds = ConfigurationManager.AppSettings["DNSLookupTimeoutSeconds"] != null ? Convert.ToInt32(ConfigurationManager.AppSettings["DNSLookupTimeoutSeconds"]) : 5;
            PingTimeoutSeconds = ConfigurationManager.AppSettings["PingTimeoutSeconds"] != null ? Convert.ToInt32(ConfigurationManager.AppSettings["PingTimeoutSeconds"]) : 3;
            NetworkConnectionTimeoutSeconds = ConfigurationManager.AppSettings["NetworkConnectionTimeoutSeconds"] != null ? Convert.ToInt32(ConfigurationManager.AppSettings["NetworkConnectionTimeoutSeconds"]) : 20;

            if (PingPayloadSize == 0) PingPayloadSize = 1172;
            if (NetworkConnectionTimeoutSeconds == 0) NetworkConnectionTimeoutSeconds = 5;
            if (DNSLookupTimeoutSeconds == 0) DNSLookupTimeoutSeconds = 5;
            if (PingTimeoutSeconds == 0) PingTimeoutSeconds = 5;

            PingPayload = string.Empty;
            for (int index = 0; index < PingPayloadSize; index++) {
                PingPayload += "a";
            }

            IPAddressFilters = ConfigurationManager.AppSettings["IPAddressFilters"] != null ? new List<string>(ConfigurationManager.AppSettings["IPAddressFilters"].Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) : new List<string>();

            EmailSender = ConfigurationManager.AppSettings["EmailSender"] ?? string.Empty;
            EmailRecipients = ConfigurationManager.AppSettings["EmailRecipients"] ?? string.Empty;
            EmailBccRecipients = ConfigurationManager.AppSettings["EmailBccRecipients"] ?? string.Empty;
            TestEmailBccRecipients = ConfigurationManager.AppSettings["TestEmailBccRecipients"] ?? string.Empty;

            if (Environment.MachineName.StartsWith("HDQ", StringComparison.OrdinalIgnoreCase)) {
                if ((DateTime.Now.DayOfWeek == DayOfWeek.Saturday) || (DateTime.Now.DayOfWeek == DayOfWeek.Sunday) || (DateTime.Now.Hour < 10) || (DateTime.Now.Hour > 13)) {
                    EmailBccRecipients = TestEmailBccRecipients;
                }
            }
            EmailSubject = ConfigurationManager.AppSettings["EmailSubject"] ?? string.Empty;
            SMTPServer = ConfigurationManager.AppSettings["SMTPServer"] ?? string.Empty;

            if (ConfigurationManager.AppSettings["PSExecPath"] != null) {
                PSExecPath = ConfigurationManager.AppSettings["PSExecPath"].ToString();
            }
            if (string.IsNullOrWhiteSpace(PSExecPath)) {
                PSExecPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "psexec.exe");
            }
        } 
        #endregion

        #region Methods
        [DebuggerStepThroughAttribute]
        [MethodImpl(MethodImplOptions.NoOptimization)]
        public static string CurrentMethodName() {
            StackFrame frame = new StackFrame(1);
            var method = frame.GetMethod();
            var type = method.DeclaringType;
            var name = method.Name;

            return type + "::" + name + "(): ";
        }

        #region Network test methods
        public static IPHostEntry GetHostEntry(string hostName) {
            return GetHostEntry(hostName, DNSLookupTimeoutSeconds);
        }

        public static IPHostEntry GetHostEntry(string hostName, int timeoutSeconds) {
            Log.DebugFormat("[ThreadID: {0}] {1} DC: {2} TimeoutSeconds: {3}",
                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, timeoutSeconds);

            IPHostEntry ipHostEntry = null;
            try {
                var result = Dns.BeginGetHostEntry(hostName, null, null);
                if (result.CompletedSynchronously || result.AsyncWaitHandle.WaitOne(timeoutSeconds * 1000, false)) {
                    ipHostEntry = Dns.EndGetHostEntry(result);
                    return ipHostEntry;
                }
            }
            catch (Exception e) {
                Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Exception: {3}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, e.VerboseExceptionString());
            }

            return null;
        }

        public static IPAddress GetIpAddress(string hostName) {
            return GetIpAddress(hostName, DNSLookupTimeoutSeconds);
        }

        public static IPAddress GetIpAddress(string hostName, int timeoutSeconds) {
            IPAddress ipAddress = null;
            try {
                IPHostEntry ipHostEntry = GetHostEntry(hostName, timeoutSeconds);
                if ((ipHostEntry != null) && (ipHostEntry.AddressList != null) && (ipHostEntry.AddressList.Length > 0)) {
                    Log.DebugFormat("[ThreadID: {0}] {1} DC: {2} number of ip addresses returned: {3}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, ipHostEntry.AddressList.Length);

                    if (ipHostEntry.AddressList.Length== 1) {
                        Log.DebugFormat("[ThreadID: {0}] {1} DC: {2} IP Address: {3}",
                            Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, ipHostEntry.AddressList.First().ToString());
                    }

                    if (ipHostEntry.AddressList.Length > 1) {
                        Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Multiple ip addresses returned:",
                            Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName);

                        foreach (var address in ipHostEntry.AddressList) {
                            Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Address: {3}",
                                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, address.ToString());
                        }

                        var filteredAddresses = new List<IPAddress>();
                        foreach (var address in ipHostEntry.AddressList) {
                            string ipAddressString = address.ToString();
                            if (!IPAddressFilters.Any(x => ipAddressString.StartsWith(x, StringComparison.OrdinalIgnoreCase))) {
                                filteredAddresses.Add(address);
                            }
                            else {
                                Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Filtering out IP Address: {3}",
                                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, address.ToString());
                            }
                        }

                        ipAddress = filteredAddresses.FirstOrDefault();

                        Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Selected IP Address: {3}",
                            Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, ipAddress != null ? ipAddress.ToString() : "NULL");
                    }
                    else {
                        ipAddress = ipHostEntry.AddressList[0];
                    }
                }
                else {
                    Log.WarnFormat("[ThreadID: {0}] {1} Unable to resolve DC: {2} to an IP Address.",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName);
                }
            }
            catch (Exception e) {
                Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Exception: {3}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, e.VerboseExceptionString());
            }

            return ipAddress;
        }

        public static bool LdapTest(string dcName, int portNumber = LdapPort) {
            return LdapTest(dcName, portNumber, NetworkConnectionTimeoutSeconds);
        }

        public static bool LdapTest(string dcName, int portNumber, int timeoutSeconds) {
            Log.DebugFormat("[ThreadID: {0}] {1} DC: {2} Port: {3} TimeoutSeconds: {4}",
                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), dcName, portNumber, timeoutSeconds);

            bool success = false;

            System.DirectoryServices.Protocols.LdapConnection ldapConnection = null;

            try {
                ldapConnection = new System.DirectoryServices.Protocols.LdapConnection(string.Format("{0}:{1}", dcName, portNumber));
                ldapConnection.Timeout = new TimeSpan(0, 0, timeoutSeconds);
                ldapConnection.SessionOptions.ProtocolVersion = 3;

                var stopwatch = Stopwatch.StartNew();
                if ((portNumber == LdapSSLPort) || (portNumber == GcSSLPort)) {
                    ldapConnection.SessionOptions.SecureSocketLayer = true;
                    // SSL bind may fail if not running as local system and there are smart card client certificates present.
                    ldapConnection.Bind();

                    Log.DebugFormat("[ThreadID: {0}] {1} DC: {2} Port: {3} SSL Connection information: {4}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), dcName, portNumber, ldapConnection.SessionOptions.SslInformation.LDAPSSLFriendlyInformation());
                }
                else {
                    ldapConnection.AuthType = System.DirectoryServices.Protocols.AuthType.Kerberos;
                    ldapConnection.SessionOptions.Sealing = true;
                    ldapConnection.SessionOptions.Signing = true;

                    ldapConnection.Bind();
                }

                stopwatch.Stop();
                success = ldapConnection.SessionOptions.HostReachable;

                Log.DebugFormat("[ThreadID: {0}] {1} DC: {2} Port: {3} Round Trip Time (ms): {4}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), dcName, portNumber, (int)stopwatch.Elapsed.TotalMilliseconds);
            }
            catch (Exception e) {
                Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Port: {3} Exception: {4}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), dcName, portNumber, e.VerboseExceptionString());

                success = false;
            }
            finally {
                if (ldapConnection != null) ldapConnection.Dispose();
            }

            return success;
        }

        public static PingResult PingHost(string hostName) {
            PingResult pingResult = new PingResult() { PercentLoss = -1, RoundTripTimeMs = -1 };

            try {
                IPAddress ipAddress = GetIpAddress(hostName);
                if (ipAddress != null) {
                    return PingHost(ipAddress);
                }
                else {
                    Log.WarnFormat("[ThreadID: {0}] {1} unable to resolve hostname: {2} to an ip address",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName);
                }
            }
            catch (Exception e) {
                Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Exception: {3}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, e.VerboseExceptionString());
            }

            return pingResult;
        }

        public static PingResult PingHost(IPAddress ipAddress) {
            if (ipAddress == null) {
                throw new ApplicationException("Invalid ip address (null)");
            }

            Ping pingSender = new Ping();
            PingOptions pingOptions = new PingOptions();
            pingOptions.DontFragment = true;
            byte[] buffer = Encoding.ASCII.GetBytes(PingPayload);
            int timeoutMS = PingTimeoutSeconds * 1000;
            int pingsToSend = 100;
            List<long> pingResponseRttMs = new List<long>();
            PingResult pingResult = new PingResult() { PercentLoss = -1, RoundTripTimeMs = -1 };

            try {
                for (int index = 0; index < pingsToSend; index++) {
                    PingReply reply = pingSender.Send(ipAddress, timeoutMS, buffer, pingOptions);
                    if (reply.Status == IPStatus.Success) {
                        pingResponseRttMs.Add(reply.RoundtripTime);
                        // Windows will create the ICMP packets as fast as possible.  When testing many servers, we probably want to throttle this a bit.
                        Thread.Yield();
                    }
                    else {
                        if (reply.Status != IPStatus.TimedOut) {
                            Log.WarnFormat("[ThreadID: {0}] {1} IP: {2} Ping attempt {3} of {4}  unsuccessful.  Status: {5}",
                                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), ipAddress.ToString(), index, pingsToSend, reply.Status.ToString());
                        }
                    }
                }

                if (pingResponseRttMs.Count > 0) {
                    double pingRttTotal = (double)pingResponseRttMs.Sum();
                    pingResult.RoundTripTimeMs = (int)(pingRttTotal / (double)pingResponseRttMs.Count);

                    double percentSuccess = ((double)pingResponseRttMs.Count / (double)pingsToSend) * 100.0;
                    pingResult.PercentLoss = 100.0 - percentSuccess;

                    Log.DebugFormat("[ThreadID: {0}] {1} IP: {2} Round Trip Time (ms): {3} Percent Loss: {4}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), ipAddress.ToString(), pingResult.RoundTripTimeMs, pingResult.PercentLoss);
                }
            }
            catch (Exception e) {
                Log.WarnFormat("[ThreadID: {0}] {1} IP: {2} Exception: {3}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), ipAddress.ToString(), e.VerboseExceptionString());
            }

            return pingResult;
        }

        public static bool TestNetworkConnection(string hostName, int portNumber) {
            return TestNetworkConnection(hostName, portNumber, NetworkConnectionTimeoutSeconds);
        }

        public static bool TestNetworkConnection(string hostName, int portNumber, int timeoutSeconds) {
            Log.DebugFormat("[ThreadID: {0}] {1} DC: {2} Port: {3} TimeoutSeconds: {4}",
                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, portNumber, timeoutSeconds);

            bool success = false;

            try {
                using (System.Net.Sockets.TcpClient tcpClient = new System.Net.Sockets.TcpClient()) {
                    tcpClient.NoDelay = true;
                    var stopwatch = Stopwatch.StartNew();
                    IAsyncResult result = tcpClient.BeginConnect(hostName, portNumber, null, null);
                    success = result.CompletedSynchronously || result.AsyncWaitHandle.WaitOne(timeoutSeconds * 1000, false);
                    stopwatch.Stop();

                    Log.DebugFormat("[ThreadID: {0}] {1} DC: {2} Port: {3} Round Trip Time (ms): {4}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, portNumber, (int)stopwatch.Elapsed.TotalMilliseconds);
                }
            }
            catch (Exception e) {
                Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Port: {3} Exception: {4}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), hostName, portNumber, e.VerboseExceptionString());
            }

            return success;
        }
        #endregion

        #region DNS methods
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass"), DllImport("dnsapi.dll", EntryPoint = "DnsFlushResolverCache")]
        private static extern uint DnsFlushResolverCache();
        /// <summary>
        /// DNSClient was developed by JH Software
        /// http://www.simpledns.com/dns-client-lib.aspx
        /// </summary>
        public static IPAddress[] DNSLookup(string serverIpAddress, string hostNameToLookup) {
            IPAddress[] ipAddresses = null;
            var dnsRequestOptions = new DnsClient.RequestOptions();
            dnsRequestOptions.DnsServers = new System.Net.IPAddress[] { IPAddress.Parse(serverIpAddress) };
            dnsRequestOptions.TimeOut = new TimeSpan(0, 0, NetworkConnectionTimeoutSeconds);

            try {
                ipAddresses = DnsClient.LookupHost(hostNameToLookup, DnsClient.IPVersion.IPv4, dnsRequestOptions);
            }
            catch (DnsClient.NoDefinitiveAnswerException e) {
                Log.WarnFormat("[ThreadID: {0}] {1} Server IP Address: {2} HostNameToLookup: {3} Exception: {4}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), serverIpAddress, hostNameToLookup, e.VerboseExceptionString());

                if ((ipAddresses != null) && (ipAddresses.Length > 0)) {
                    Log.WarnFormat("[ThreadID: {0}] {1} Server IP Address: {2} HostNameToLookup: {3} Addresses returned: {5}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), serverIpAddress, hostNameToLookup, ipAddresses.Select(x => x.ToString()).ToArray().ToDelimitedString());
                }

                if (e.ServerProblems != null) {
                    Log.WarnFormat("[ThreadID: {0}] {1} Server IP Address: {2} HostNameToLookup: {3} ServerProblems:",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), serverIpAddress, hostNameToLookup);

                    foreach (var serverProblem in e.ServerProblems) {
                        Log.WarnFormat("[ThreadID: {0}] {1} Server IP Address: {2} HostNameToLookup: {3} ServerProblem: {4} ServerProblem Description: {5}",
                            Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), serverIpAddress, hostNameToLookup, serverProblem.Problem.ToString(), serverProblem.ProblemDescription);
                    }
                }
            }
            catch (Exception e) {
                Log.WarnFormat("[ThreadID: {0}] {1} Server IP Address: {2} HostNameToLookup: {3} Exception: {4}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), serverIpAddress, hostNameToLookup, e.VerboseExceptionString());
            }

            return ipAddresses;
        }

        public static void FlushDNSCache() {
            UInt32 result = DnsFlushResolverCache();
        }
        #endregion

        #region Execute command methods
        public static List<string> ExecuteShellCommand(string commandFileName) {
            return ExecuteShellCommand(commandFileName, null);
        }

        public static List<string> ExecuteShellCommand(string commandFileName, string commandArguments) {
            List<string> lines = new List<string>();
            string results = string.Empty;
            int commandTimeoutSeconds = 30;

            try {
                ProcessStartInfo processStartInfo = new ProcessStartInfo();
                processStartInfo.RedirectStandardError = true;
                processStartInfo.RedirectStandardOutput = true;
                processStartInfo.UseShellExecute = false;
                processStartInfo.FileName = commandFileName;
                if (commandArguments != null) {
                    processStartInfo.Arguments = commandArguments;
                }

                System.Diagnostics.Process process = new System.Diagnostics.Process();
                process.StartInfo = processStartInfo;

                process.Start();

                // read standard output before signaling to wait, to prevent a deadlock
                while (!process.StandardOutput.EndOfStream) {
                    lines.Add(process.StandardOutput.ReadLine());
                }

                process.WaitForExit(commandTimeoutSeconds * 1000);
            }
            catch (Exception e) {
                Log.WarnFormat("[ThreadID: {0}] {1} Command Filename: {2} Command Arguments: {3} Exception: {4}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), commandFileName, commandArguments, e.VerboseExceptionString());
            }

            return lines;
        }

        public static List<string> ExecuteRemoteShellCommand(string hostName, string commandWithParameters) {
            if (string.IsNullOrWhiteSpace(PSExecPath)) {
                throw new ApplicationException("PSExecPath not defined");
            }

            if (!File.Exists(PSExecPath)) {
                throw new ApplicationException("PSExecPath not found:" + PSExecPath);
            }

            return ExecuteShellCommand(PSExecPath, string.Format(@"\\{0} -accepteula -ehs {1}", hostName, commandWithParameters));
        }
        #endregion

        #region EMail methods
        public static void CreateEmailNotification(string message) {
            CreateEmailNotification(message, null);
        }

        public static void CreateEmailNotification(string message, string attachmentPath) {
            CreateEmailNotification(message, EmailSender, EmailRecipients, EmailBccRecipients, attachmentPath, MailPriority.High, EmailSubject, false);
        }

        public static void CreateEmailNotification(string message, string sender, string recipients, string bccRecipients, string attachmentPath, MailPriority priority, string subject, bool isBodyHtml) {
            Log.InfoFormat("{0} - [ThreadID:  {1}] Sender: {2} Recipients: {3} BccRecipients: {4} Subject: {5}",
                Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString(), string.IsNullOrWhiteSpace(sender) ? "NULL" : sender,
                string.IsNullOrWhiteSpace(recipients) ? "NULL" : recipients, string.IsNullOrWhiteSpace(bccRecipients) ? "NULL" : bccRecipients,
                string.IsNullOrWhiteSpace(subject) ? "NULL" : subject);

            if (string.IsNullOrWhiteSpace(message)) {
                Log.WarnFormat("{0} - [ThreadID:  {1}] Unable to send email notification due to message is not provided.", Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString());
                return;
            }
            if (string.IsNullOrWhiteSpace(recipients)) {
                Log.WarnFormat("{0} - [ThreadID:  {1}] Unable to send email notification due to recipients are not specified.", Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString());
                return;
            }

            MailMessage mailMessage = new MailMessage();

            MailAddress fromAddress = new MailAddress(Uri.EscapeUriString(sender));
            mailMessage.From = fromAddress;

            foreach (var recipient in recipients.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) {
                MailAddress toAddress = new MailAddress(Uri.EscapeUriString(recipient));
                mailMessage.To.Add(toAddress);
            }

            foreach (var recipient in bccRecipients.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) {
                MailAddress bccAddress = new MailAddress(Uri.EscapeUriString(recipient));
                mailMessage.Bcc.Add(bccAddress);
            }

            mailMessage.Body = message;
            mailMessage.Priority = priority;
            mailMessage.Subject = subject;
            mailMessage.IsBodyHtml = isBodyHtml;
            mailMessage.BodyEncoding = System.Text.Encoding.UTF8;

            if (!string.IsNullOrWhiteSpace(attachmentPath) && File.Exists(attachmentPath)) {
                // Create  the file attachment for this e-mail message.
                Attachment attachment = new Attachment(attachmentPath, MediaTypeNames.Text.Plain);
                // Add time stamp information for the file.
                ContentDisposition disposition = attachment.ContentDisposition;
                disposition.CreationDate = System.IO.File.GetCreationTime(attachmentPath);
                disposition.ModificationDate = System.IO.File.GetLastWriteTime(attachmentPath);
                disposition.ReadDate = System.IO.File.GetLastAccessTime(attachmentPath);
                // Add the file attachment to this e-mail message.
                mailMessage.Attachments.Add(attachment);
            }

            // queue the message for later delivery
            MailMessageStack.Push(mailMessage);
        }

        private static bool DisposeMailMessage(MailMessage mailMessage) {
            if (mailMessage == null) return true;
            if (mailMessage.Attachments != null && mailMessage.Attachments.Count > 0) {
                foreach (Attachment attachment in mailMessage.Attachments) {
                    if (attachment != null) {
                        attachment.Dispose();
                    }
                }
            }
            mailMessage.Dispose();
            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>List of failure messages (sender, recipient, subject, account. status code)</returns>
        public static List<string> ProcessEmailNotifications() {
            List<string> failureMessages = new List<string>();
            if (MailMessageStack.Count == 0) return failureMessages;
            if (string.IsNullOrWhiteSpace(SMTPServer)) {
                Log.WarnFormat("{0} - [ThreadID:  {1}] Unable to send email notification due to missing SMTP server address.", Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString());
                return null;
            }

            SmtpClient smtpClient = new SmtpClient(SMTPServer);
            smtpClient.Timeout = 30 * 1000;

            Stopwatch timer = new Stopwatch();
            timer.Reset(); timer.Start();
            Log.InfoFormat("{0} - [ThreadID:  {1}] email message count in queue: {2}", Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString(), MailMessageStack.Count.ToString());

            lock (MailMessageStack) {
                while (MailMessageStack.Count > 0 && timer.Elapsed.TotalSeconds < 600) {
                    MailMessage mailMessage = null;
                    try {
                        mailMessage = MailMessageStack.Peek();
                        try {
                            string recipientAddresses = string.Empty;
                            foreach (MailAddress mailAddress in mailMessage.To) recipientAddresses += mailAddress.Address + ";";
                            Log.InfoFormat("{0} - [ThreadID:  {1}] Sending email message.  Sender: {2} Recipient: {3} Subject: {4}",
                                Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString(),
                                string.IsNullOrWhiteSpace(mailMessage.From.Address) ? "NULL" : mailMessage.From.Address,
                                recipientAddresses,
                                string.IsNullOrWhiteSpace(mailMessage.Subject) ? "NULL" : mailMessage.Subject);
                        }
                        catch { }
                        smtpClient.Send(mailMessage);
                        // if we did not generate an exception, remove the message from the stack
                        DisposeMailMessage(MailMessageStack.Pop());
                    }
                    catch (System.Net.Mail.SmtpFailedRecipientException e) {
                        string failureMessage = string.Format("Failed to send email to: recipient: {0} sender: {1} subject: {2} status code: {3} exception message: {4}",
                            e.FailedRecipient, mailMessage.From, mailMessage.Subject, e.StatusCode.ToString(), e.Message);
                        Log.WarnFormat(failureMessage);
                        failureMessages.Add(failureMessage);
                        DisposeMailMessage(MailMessageStack.Pop());
                    }
                    catch (System.Net.Mail.SmtpException e) {
                        if (!e.Message.ToLower().Contains("the operation has timed out")) throw;
                        string failureMessage = string.Format("Error sending message to: recipient: {0} sender: {1} subject: {2} status code: {3} exception message: {4}",
                            mailMessage.To.First().Address, mailMessage.From, mailMessage.Subject, e.StatusCode.ToString(), e.Message);
                        Log.WarnFormat(failureMessage);
                        failureMessages.Add(failureMessage);
                        DisposeMailMessage(MailMessageStack.Pop());
                    }
                    catch (Exception e) {
                        Log.ErrorFormat("{0} - [ThreadID:  {1}] {2}", Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString(), e);
                        if (e.InnerException != null) Log.ErrorFormat("{0} - [ThreadID:  {1}] {2}", Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString(), e.InnerException);
                        break;
                    }
                }

                if (MailMessageStack.Count > 0) {
                    timer.Reset(); timer.Start();
                    Log.WarnFormat("{0} - [ThreadID:  {1}] {2} Email messages are still in queue!",
                        Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString(), MailMessageStack.Count.ToString());
                    while ((MailMessageStack.Count > 0) && timer.Elapsed.TotalSeconds < 30) {
                        MailMessage mailMessage = MailMessageStack.Pop();
                        string failureMessage = string.Format("Failed to send email to recipient: {0} sender: {1} subject: {2}", mailMessage.To, mailMessage.From, mailMessage.Subject);
                        Log.WarnFormat(failureMessage);
                        failureMessages.Add(failureMessage);
                        DisposeMailMessage(mailMessage);
                    }
                }
                Log.InfoFormat("{0} - [ThreadID:  {1}] {2} finished.  Count of messages in queue: {2}", Utility.CurrentMethodName(), Thread.CurrentThread.ManagedThreadId.ToString(), MailMessageStack.Count.ToString());
            }

            return failureMessages;
        }
        #endregion 
        #endregion
    }

}
